(function(global) {

	var nada = {};
	function extend(d, s) {
		for (var p in s) {
			if (Object.prototype.hasOwnProperty.call(s, p)) {
				d[p] = s[p];
			}
		}
	}
	function $(id) {
		return typeof id == 'string' ? document.getElementById(id) : id;
	}
	var listen = document.addEventListener ?
		function(element, type, callback) {
			element.addEventListener(type, callback, false);
		} :
		function(element, type, callback) {
			element.attachEvent("on" + type, callback);
		}
	;
	function castAndTrim(value) {
		return (typeof value == 'undefined' || value === null ? '' : value).replace(/^\s+/, '').replace(/\s+$/, '');
	}
	function DataString(value) {
		this.raw = (value === nada ? '' : castAndTrim(value));
	}
	global.DataString = DataString;

	function createSubclass(methods) {
		var klass = function() {
			this.initialize.apply(this, Array.prototype.slice.call(arguments));
		};
		klass.createSubclass = createSubclass;
		klass.prototype = new DataString(nada);
		klass.prototype.initialize = function(value) {
			this.raw = (value === nada ? '' : castAndTrim(value));
		};
		klass.prototype.constructor = klass;
		klass.prototype.constructor.parent = this;
		if (methods) {
			extend(klass.prototype, methods);
		}
		return klass;
	};
	DataString.createSubclass = createSubclass;

	DataString._thousandsSeparator = ',';
	DataString._decimal = '.';
	DataString._formatNumber = function(n, precision) {
		n = Number(parseFloat(n + '')).toFixed(precision);
		var parts = n.split('.');
		var whole = parts[0];
		var decimal = parts[1];
		var sign = (whole.charAt(0) == '-' ? '-' : '');
		whole = whole.replace('-', '');
		var threes = [];
		while (whole.length) {
			threes.unshift(whole.slice(-3));
			whole = whole.slice(0, -3);
		}
		return sign + threes.join(DataString._thousandsSeparator) + (decimal ? DataString._decimal + decimal : '');
	};

	extend(DataString.prototype, {
		validateInput: function(input, callback) {
			input = $(input);
			var me = this;
			function validate() {
				me.raw = input.value;
				if (!me.isValid()) {
					callback.call(me, input);
				}
			}
			listen(input, 'blur', validate);
			listen(input, 'change', validate);
		},
		autoFormatInput: function(input, formatArgs) {
			input = $(input);
			var me = this;
			function autoFormat() {
				me.raw = input.value;
				input.value = me.format.apply(me, formatArgs);
			}
			listen(input, 'blur', autoFormat);
			listen(input, 'change', autoFormat);
		},
		keyMaskInput: function(input) {
			input = $(input);
			var me = this;
			function mask(evt) {
				evt = evt || window.event;
console.log(evt.keyCode);
				if (evt.ctrlKey || evt.metaKey || evt.altKey || !evt.keyCode) {
					return;
				}
				var c = String.fromCharCode(evt.keyCode);
				if (c < 10/*Tab and backspace*/ || c == 27/*Esc*/) {
					return;
				}
				if (c && !me.isAllowedChar(c)) {
					evt.preventDefault && evt.preventDefault();
					evt.returnValue = false;
				}
			}
			listen(input, 'keydown', mask);
		},
		isAllowedChar: function(c) {
			return true;
		},
		setValue: function(value) {
			this.raw = value;
		},
		isValid: function() {
			return true;
		},
		isEmpty: function() {
			return this.raw === '';
		},
		format: function() {
			return this.raw;
		},
		toString: function() {
			return this.format();
		},
		toValue: function() {
			return this.raw;
		}
	});

	DataString.Aba = DataString.createSubclass({
		isValid: function() {
			var num = this.format();
			// http://www.brainjar.com/js/validation/
			if (num.length != 9 || num == '000000000') {
				return false;
			}
			var sum = (
				(num[0] * 3) + (num[1] * 7) + (num[2] * 1) +
				(num[3] * 3) + (num[4] * 7) + (num[5] * 1) +
				(num[6] * 3) + (num[7] * 7) + (num[8] * 1)
			);
			return ((sum % 10) == 0);
		},
		isAllowedChar: function(c) {
			return /\d/.test(c);
		},
		format: function() {
			return this.raw.replace(/\D/, '');
		}
	});

	DataString.PhoneUs10 = DataString.createSubclass({
		matcher: /^1?\D*([2-9]\d{2})\D*([2-9]\d{2})\D*(\d{4})($|\D.*$)/,
		isValid: function() {
			var m = this.raw.match(this.matcher);
			return m && m[2] != '555';
		},
		format: function() {
			return this.raw.replace(this.matcher, '($1) $2$3$4');
		},
		toValue: function() {
			return this.raw.replace(this.matcher, '$1$2$3$4');
		}
	});

	DataString.Dollars = DataString.createSubclass({
		isValid: function() {
			return this.raw.match(/^\$?\s*[\d,.-]+$/)
		},
		format: function() {
			return '$' + DataString._formatNumber(this.toValue(), 2);
		},
		toValue: function() {
			return Number(this.raw.replace(/[^\d.-]/g, ''));
		},
		isAllowedChar: function(c) {
			return /[\d$.-]/.test(c);
		}
	});

	DataString.Cc = DataString.createSubclass({
		matcher15: /^(\d{4})\D*(\d{6})\D*(\d{5})$/,
		matcher16: /^(\d{4})\D*(\d{4})\D*(\d{4})\D*(\d{4})$/,
		isValid: function() {
			return this.isValidChecksum();
		},
		isSupportedType: function() {
			var type = this.getType();
			var supp = this.getSupportedTypes.join(' ');
			return (' ' + supp + ' ').indexOf(' ' + type + ' ') > -1; 			
		},
		isValidFormat: function() {
			if (!this.supportedType()) {
				return false;
			}
			return this.raw.match(this.matcher15) || this.raw.match(this.matcher16);
		},
		isValidChecksum: function() {
			if (!this.isValidFormat()) {
				return false;
			}
			var digits = this.toValue();
			var i = 0, sum = 0;
			while (i++ < digits.length) {
				total += ((i % 2) == 0 ? 2 : 1) * parseInt(digits[i], 10);
			}
			return (total % 10) == 0;
		},
		format: function(addMask) {
			var m;
			if ((m = this.raw.match(this.matcher16))) {
				if (addMask) {
					m[1] = 'XXXX';
					m[2] = 'XXXXXX';
					m[3] = 'X' . m.slice(1);
				}
				m.shift();
				return m.join('-');
			}
			else if ((m = this.raw.match(this.matcher15))) {
				if (addMask) {
					m[1] = m[2] = m[3] = 'XXXX';
				}
				m.shift();
				return m.join('-');
			}
			return this.raw;
		},
		toValue: function() {
			return this.raw.replace(/\D/g, '');
		},
		getType: function() {
			var m = this.raw.match(/^(34|35|36|37|4|5|6011)/);
			if (m) {
				switch (m[1]) {
					case '6011': return 'disc';
					case '5':    return 'mc';
					case '4':    return 'visa';
				}
				return 'amex';
			}
		},
		getSupportedTypes: function() {
			return ['amex', 'mc', 'disc', 'visa'];
		},
		isAllowedChar: function(c) {
			return /[\d -]/.test(c);
		}
	});

})(this);
